██╗ ██╗███████╗██████╗ ███████╗██╗ ██╗
██║ ██║██╔════╝██╔══██╗██╔════╝╚██╗██╔╝
██║ ██║███████╗██║ ██║█████╗ ╚███╔╝
██║ ██║╚════██║██║ ██║██╔══╝ ██╔██╗
███████╗██║███████║██████╔╝███████╗██╔╝ ██╗
╚══════╝╚═╝╚══════╝╚═════╝ ╚══════╝╚═╝ ╚═╝A keyless memory-corruption scanner for C and C++ parsers in Rust. Point it at a source tree and it ranks which functions actually parse untrusted bytes, writes a fuzz harness for the best one with no model in the loop, builds the library under AddressSanitizer without a hand-written build script, runs an AFL++ campaign, then collapses the crash pile into distinct bugs with a stack-hash key, hand-labelled severity, a minimized reproducer proven to still trigger the same bug, and a coordinated-disclosure advisory in human and machine form. It needs no API key, no account and no cloud, and every stage refuses out loud rather than guessing.
Memory corruption in file-format and media parsers is the CVE class that refuses to die. These libraries are old C, they run on bytes an attacker fully controls, and they sit underneath everything: an image decoder is reachable from a browser tab, a chat app, a thumbnailer and a mail client at the same time. CVE-2023-4863, a heap buffer overflow in libwebp's Huffman decoder, was exploited in the wild and forced emergency out-of-band patches across Chrome, Firefox, Android and Electron in September 2023, because a single parser is a shared dependency of half the internet. Stagefright did the same to Android in 2015 through the media parser, reachable by MMS.
Fuzzing is how these get found, and fuzzing itself is a solved commodity. AFL++ is free, fast and excellent. The expensive parts are the two on either side of it. Somebody has to decide which of nine hundred exported functions takes attacker bytes and write a harness that drives it correctly, and somebody has to turn the resulting pile of thousands of crash files into the handful of distinct bugs it actually represents, with a reproducer a maintainer will accept. Both are hand work, both need someone who already knows the library, and both are why most C projects have no fuzz target at all.
lisdex automates the two ends and drives AFL++ in the middle. It is validated against Magma, a benchmark of real historical CVEs re-planted into real libraries with canaries that report which specific bug a crash reached, so "it found something" can be checked against ground truth rather than asserted.
source tree -> entry points -> harness -> instrumented build -> campaign
-> crash pile -> distinct bugs -> severity -> advisory + reproducer
A real campaign against libpng: 182 crash files, 2 distinct bugs. Getting from the first number to the second is most of what this tool is for.
curl -fsSL https://angelamos.com/lisdex/install.sh | bashOne command, zero further steps: it installs the Rust toolchain and AFL++ if they
are missing, builds lisdex, drops it on your PATH, and leaves lisdex runnable
by name. No API key, no account, no cloud. Then point it at a tree:
# what in this tree parses untrusted bytes?
lisdex surface ./libpng
# build it under AddressSanitizer with no hand-written build script
lisdex build https://github.com/glennrp/libpng --work /tmp/lisdex
# fuzz it
lisdex fuzz --target /tmp/lisdex/builds/libpng/afl/harness \
--seeds ./corpus --findings /tmp/out --db /tmp/lisdex.sqlite
# watch it
lisdex tui --db /tmp/lisdex.sqlite --findings /tmp/out
lisdex serve --db /tmp/lisdex.sqlite --findings /tmp/out
# what did it find, and how bad?
lisdex crashes --db /tmp/lisdex.sqlite --campaign 1
# draft the disclosure
lisdex bundle --db /tmp/lisdex.sqlite --campaign 1 --out /tmp/bundleslisdex bundle writes one directory per distinct bug: the advisory in human and
machine form, the exact crashing bytes, the sanitizer report, and an executable
reproduce.sh.
| Command | What it does |
|---|---|
surface |
Rank the parser entry points of a source tree |
harness |
Synthesize a harness for a ranked entry point, with no LLM |
verify |
Run the four harness oracles against one harness source file |
build |
Auto-build a target under sanitizers |
fuzz |
Run an AFL++ campaign |
coverage |
Report the coverage curve a campaign recorded |
corpus |
Distil a corpus to the smallest set reaching the same edges |
crashes |
List the crashes persisted for a campaign |
import |
Persist the crashes of a findings directory a killed campaign left behind |
minimize |
Minimize one reproducer per distinct bug |
advisory |
Draft a coordinated-disclosure advisory per distinct bug |
bundle |
Write a self-contained reproducer bundle per distinct bug |
tui |
Terminal dashboard over a live campaign |
serve |
Local web console over the same state |
bench |
Compare two builds over repeated trials, with a Mann-Whitney U test |
rank-eval |
Score the cold-start ranking against the entry points real harnesses call |
llm |
Report which optional booster tier is configured |
These are deliberate, and they are the reason to trust the rest of the output.
- It will not key a crash it cannot symbolize. An unsymbolized frame is a module plus an offset, shared by every crash of that kind in that module. Bucketing on it merges unrelated bugs. lisdex reports the crash unkeyed instead.
- It will not guess an affected version range, a CVSS score, or attacker reachability. A campaign fuzzed one build. Those fields are required keys typed string-or-null, and both CVE and GHSA accept an unknown.
- It will not emit a harness that fuzzes handle plumbing. If an entry point needs an opaque context lisdex cannot build, it refuses and says which parameter blocked it.
- It will not report a planted bug. Crashes from a benchmark-instrumented build are stamped NOT DISCLOSABLE with no filing route.
lisdex works fully keyless. A local model can be attached as an optional booster, and it only ever proposes an answer that lisdex's own oracles then accept or refuse.
just ollama-up
just ollama-model # qwen2.5-coder:14b-instruct-q6_K, Apache-2.0
lisdex llm --askThere is no degraded keyless mode. The booster is an accelerator, never a requirement.
Linux, a C compiler, and AFL++. The installer handles all three, including
libclang, which lisdex surface loads at runtime.
lisdex drives AFL++ as a separate process and does not link or vendor it. AFL++ is AGPL-3.0-or-later; lisdex is AGPL-3.0; the installer fetches AFL++ upstream and unmodified.
The learn/ folder is the long version.
- 00-OVERVIEW.md - what this is and how to run it
- 01-CONCEPTS.md - memory corruption, sanitizers, why crash counts lie, what disclosure requires
- 02-ARCHITECTURE.md - the pipeline and the design decisions that cost the most
- 03-IMPLEMENTATION.md - how each stage works
- 04-CHALLENGES.md - extensions, and two things not to build
just dev-up # the toolchain container
just dev-verify # prove AFL++, ASAN, cmplog and the LTO pass all work
just test # the workspace suite
just check # fmt, clippy -D warnings, tests
just magma-libpng # build the Magma validation rig
just m9-gate # one milestone's exit gate, end to endEverything runs in Docker. The exit gates run real campaigns against a Magma-instrumented libpng and check the result against canary ground truth.
AGPL-3.0.